package org.ops.ui.editor.swing.ash;

/*
 * KeywordMap.java - Fast keyword->id map
 * Copyright (C) 1998, 1999 Slava Pestov
 * Copyright (C) 1999 Mike Dillon
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */

import javax.swing.text.Segment;
import java.util.regex.Pattern;

/**
 * A <code>KeywordMap</code> is similar to a hashtable in that it maps keys
 * to values. However, the `keys' are Swing segments. This allows lookups of
 * text substrings without the overhead of creating a new string object.
 * <p>
 * This class is used by <code>CTokenMarker</code> to map keywords to ids.
 *
 * @author Slava Pestov, Mike Dillon
 * @version $Id: KeywordMap.java,v 1.3 2007-03-02 00:02:42 meboyce Exp $
 */
public class KeywordMap {
	public static final Pattern NUMBER_PATTERN = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?([FfDdLl])?");
	public static final Pattern HEX_NUMBER_PATTERN = Pattern.compile("0x[0-9A-Fa-f]+");
  /**
   * Creates a new <code>KeywordMap</code>.
   * @param ignoreCase True if keys are case insensitive
   */
  public KeywordMap(boolean ignoreCase) {
    this(ignoreCase, 52);
    this.ignoreCase = ignoreCase;
  }

  /**
   * Creates a new <code>KeywordMap</code>.
   * @param ignoreCase True if the keys are case insensitive
   * @param mapLength The number of `buckets' to create.
   * A value of 52 will give good performance for most maps.
   */
  public KeywordMap(boolean ignoreCase, int mapLength) {
    this.mapLength = mapLength;
    this.ignoreCase = ignoreCase;
    map = new Keyword[mapLength];
  }

  /**
   * Looks up a key.
   * @param text The text segment
   * @param offset The offset of the substring within the text segment
   * @param length The length of the substring
   */
  public byte lookup(Segment text, int offset, int length) {
    if(length == 0)
      return Token.NULL;
    Keyword k = map[getSegmentMapKey(text, offset, length)];
    while(k != null) {
      if(length != k.keyword.length) {
        k = k.next;
        continue;
      }
      if(SyntaxUtilities.regionMatches(ignoreCase,text,offset,
            k.keyword))
        return k.id;
      k = k.next;
    }
		String token = new String(text.array, offset, length);
		if(NUMBER_PATTERN.matcher(token).matches() ||
			 HEX_NUMBER_PATTERN.matcher(token).matches())
			return Token.LITERAL3;
   	return Token.NULL;
  }

  /**
   * Adds a key-value mapping.
   * @param keyword The key
   * @Param id The value
   */
  public void add(String keyword, byte id) {
    int key = getStringMapKey(keyword);
    map[key] = new Keyword(keyword.toCharArray(),id,map[key]);
  }

  /**
   * Returns true if the keyword map is set to be case insensitive,
   * false otherwise.
   */
  public boolean getIgnoreCase() {
    return ignoreCase;
  }

  /**
   * Sets if the keyword map should be case insensitive.
   * @param ignoreCase True if the keyword map should be case
   * insensitive, false otherwise
   */
  public void setIgnoreCase(boolean ignoreCase) {
    this.ignoreCase = ignoreCase;
  }

  // protected members
  protected int mapLength;

  protected int getStringMapKey(String s) {
    return (Character.toUpperCase(s.charAt(0)) +
        Character.toUpperCase(s.charAt(s.length()-1)))
      % mapLength;
  }

  protected int getSegmentMapKey(Segment s, int off, int len) {
    return (Character.toUpperCase(s.array[off]) +
        Character.toUpperCase(s.array[off + len - 1]))
      % mapLength;
  }

  // private members
  class Keyword {
    public Keyword(char[] keyword, byte id, Keyword next) {
      this.keyword = keyword;
      this.id = id;
      this.next = next;
    }

    public char[] keyword;
    public byte id;
    public Keyword next;
  }

  private Keyword[] map;
  private boolean ignoreCase;
}
